package com.lipaluma.field.validator;

import static org.joox.JOOX.$;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.joda.time.DateTime;
import org.joox.Match;
import org.w3c.dom.Element;

import com.lipaluma.Xml;
import com.lipaluma.annotations.validations.Required;
import com.lipaluma.annotations.validations.Validate;
import com.lipaluma.annotations.validations.ValidateBoolean;
import com.lipaluma.annotations.validations.ValidateDate;
import com.lipaluma.annotations.validations.ValidateDecimal;
import com.lipaluma.annotations.validations.ValidateInteger;
import com.lipaluma.context.Context;
import com.lipaluma.exceptions.CsvParserException;
import com.lipaluma.field.validator.impl.Enum.EnumConstantValidator;
import com.lipaluma.field.validator.impl.Enum.EnumExcludedOfValidator;
import com.lipaluma.field.validator.impl.Enum.EnumIncludedInValidator;
import com.lipaluma.field.validator.impl.Enum.EnumUniqueValidator;
import com.lipaluma.field.validator.impl.bool.BooleanConstantValidator;
import com.lipaluma.field.validator.impl.collections.CollectionNotEmptyValidator;
import com.lipaluma.field.validator.impl.collections.CollectionSizeValidator;
import com.lipaluma.field.validator.impl.date.AfterDateValidator;
import com.lipaluma.field.validator.impl.date.BeforeDateValidator;
import com.lipaluma.field.validator.impl.date.DateConstantValidator;
import com.lipaluma.field.validator.impl.date.DateExcludedOfValidator;
import com.lipaluma.field.validator.impl.date.DateIncludedInValidator;
import com.lipaluma.field.validator.impl.date.DateTimeConstantValidator;
import com.lipaluma.field.validator.impl.date.DateTimeExcludedOfValidator;
import com.lipaluma.field.validator.impl.date.DateTimeIncludedInValidator;
import com.lipaluma.field.validator.impl.date.DateUniqueValidator;
import com.lipaluma.field.validator.impl.numeric.DConstantValidator;
import com.lipaluma.field.validator.impl.numeric.DExcludedOfValidator;
import com.lipaluma.field.validator.impl.numeric.DIncludedInValidator;
import com.lipaluma.field.validator.impl.numeric.DMinMaxValidator;
import com.lipaluma.field.validator.impl.numeric.DUniqueValidator;
import com.lipaluma.field.validator.impl.numeric.LConstantValidator;
import com.lipaluma.field.validator.impl.numeric.LExcludedOfValidator;
import com.lipaluma.field.validator.impl.numeric.LIncludedInValidator;
import com.lipaluma.field.validator.impl.numeric.LMinMaxValidator;
import com.lipaluma.field.validator.impl.numeric.LUniqueValidator;
import com.lipaluma.field.validator.impl.string.StringConstantValidator;
import com.lipaluma.field.validator.impl.string.StringContainsValidator;
import com.lipaluma.field.validator.impl.string.StringEndsWithValidator;
import com.lipaluma.field.validator.impl.string.StringExcludedOfValidator;
import com.lipaluma.field.validator.impl.string.StringIncludedInValidator;
import com.lipaluma.field.validator.impl.string.StringLengthValidator;
import com.lipaluma.field.validator.impl.string.StringMatchesValidator;
import com.lipaluma.field.validator.impl.string.StringNotContainsValidator;
import com.lipaluma.field.validator.impl.string.StringNotEndsWithValidator;
import com.lipaluma.field.validator.impl.string.StringNotStartsWithValidator;
import com.lipaluma.field.validator.impl.string.StringStartsWithValidator;
import com.lipaluma.field.validator.impl.string.StringUniqueValidator;
import com.lipaluma.util.ReflectionUtils;
import com.lipaluma.xml.mapping.XmlFieldMapping;
import com.lipaluma.xml.validator.XmlCustomValidator;
import com.lipaluma.xml.validator.XmlValidator;

@SuppressWarnings("unchecked")
public class ValidatorFactory {
	public static final String VALIDATION_UNIQUE_FUNCTION = "VALIDATION_UNIQUE_FUNCTION";
	public static final String VALIDATION_CONSTANT_FUNCTION = "VALIDATION_CONSTANT_FUNCTION";
	public static final String VALIDATION_LENGTH_FUNCTION = "VALIDATION_LENGTH_FUNCTION";
	public static final String VALIDATION_MATCHES_FUNCTION = "VALIDATION_MATCHES_FUNCTION";
	public static final String VALIDATION_CONTAINS_FUNCTION = "VALIDATION_CONTAINS_FUNCTION";
	public static final String VALIDATION_NOT_CONTAINS_FUNCTION = "VALIDATION_NOT_CONTAINS_FUNCTION";
	public static final String VALIDATION_ENDS_WITH_FUNCTION = "VALIDATION_ENDS_WITH_FUNCTION";
	public static final String VALIDATION_NOT_ENDS_WITH_FUNCTION = "VALIDATION_NOT_ENDS_WITH_FUNCTION";
	public static final String VALIDATION_STARTS_WITH_FUNCTION = "VALIDATION_STARTS_WITH_FUNCTION";
	public static final String VALIDATION_NOT_STARTS_WITH_FUNCTION = "VALIDATION_NOT_STARTS_WITH_FUNCTION";
	public static final String VALIDATION_INCLUDED_IN_FUNCTION = "VALIDATION_INCLUDED_IN_FUNCTION";
	public static final String VALIDATION_EXCLUDED_OF_FUNCTION = "VALIDATION_EXCLUDED_OF_FUNCTION";
	public static final String VALIDATION_AFTER_FUNCTION = "VALIDATION_AFTER_FUNCTION";
	public static final String VALIDATION_BEFORE_FUNCTION = "VALIDATION_BEFORE_FUNCTION";
	public static final String VALIDATION_MIN_MAX_FUNCTION = "VALIDATION_MIN_MAX_FUNCTION";
	public static final String VALIDATION_NOT_EMPTY_FUNCTION = "VALIDATION_NOT_EMPTY_FUNCTION";
	
	private static Map<Class<?>, Class<? extends Annotation>> mapAnnotationByType = new HashMap<Class<?>, Class<? extends Annotation>>();

	private static Map<Class<?>, List<Class<? extends Validator>>> mapValidatorByType = new HashMap<Class<?>, List<Class<? extends Validator>>>();
	private static Map<String, List<Class<? extends Validator>>> mapValidatorByFunction = new HashMap<String, List<Class<? extends Validator>>>();
	private static Class<? extends Validator>[] defaultEnumValidatorType;
	
	static {
		mapAnnotationByType.put(Boolean.class, ValidateBoolean.class);
		mapAnnotationByType.put(boolean.class, ValidateBoolean.class);
		mapAnnotationByType.put(String.class, Validate.class);
		mapAnnotationByType.put(Date.class, ValidateDate.class);
		mapAnnotationByType.put(DateTime.class, ValidateDate.class);
		mapAnnotationByType.put(short.class, ValidateInteger.class);
		mapAnnotationByType.put(Short.class, ValidateInteger.class);
		mapAnnotationByType.put(int.class, ValidateInteger.class);
		mapAnnotationByType.put(Integer.class, ValidateInteger.class);
		mapAnnotationByType.put(long.class, ValidateInteger.class);
		mapAnnotationByType.put(Long.class, ValidateInteger.class);
		mapAnnotationByType.put(float.class, ValidateDecimal.class);
		mapAnnotationByType.put(Float.class, ValidateDecimal.class);
		mapAnnotationByType.put(Double.class, ValidateDecimal.class);
		mapAnnotationByType.put(double.class, ValidateDecimal.class);
		mapAnnotationByType.put(Enum.class, Validate.class);
		
		putValidatorsForType(Enum.class, EnumConstantValidator.class, EnumUniqueValidator.class, EnumIncludedInValidator.class, EnumExcludedOfValidator.class);
		putValidatorsForType(Boolean.class, BooleanConstantValidator.class);
		putValidatorsForType(boolean.class, BooleanConstantValidator.class);
		putValidatorsForType(String.class, StringUniqueValidator.class, StringLengthValidator.class, StringContainsValidator.class, StringNotContainsValidator.class, StringEndsWithValidator.class, StringNotEndsWithValidator.class, StringStartsWithValidator.class, StringNotStartsWithValidator.class, StringMatchesValidator.class, StringConstantValidator.class, StringIncludedInValidator.class, StringExcludedOfValidator.class);
		putValidatorsForType(Date.class, DateUniqueValidator.class, AfterDateValidator.class, BeforeDateValidator.class, DateConstantValidator.class, DateExcludedOfValidator.class, DateIncludedInValidator.class);
		putValidatorsForType(DateTime.class, DateUniqueValidator.class, AfterDateValidator.class, BeforeDateValidator.class, DateTimeConstantValidator.class, DateTimeExcludedOfValidator.class, DateTimeIncludedInValidator.class);
		putValidatorsForType(short.class, LConstantValidator.class, LExcludedOfValidator.class, LIncludedInValidator.class, LMinMaxValidator.class, LUniqueValidator.class);
		putValidatorsForType(Short.class, LConstantValidator.class, LExcludedOfValidator.class, LIncludedInValidator.class, LMinMaxValidator.class, LUniqueValidator.class);
		putValidatorsForType(int.class, LConstantValidator.class, LExcludedOfValidator.class, LIncludedInValidator.class, LMinMaxValidator.class, LUniqueValidator.class);
		putValidatorsForType(Integer.class, LConstantValidator.class, LExcludedOfValidator.class, LIncludedInValidator.class, LMinMaxValidator.class, LUniqueValidator.class);
		putValidatorsForType(long.class, LConstantValidator.class, LExcludedOfValidator.class, LIncludedInValidator.class, LMinMaxValidator.class, LUniqueValidator.class);
		putValidatorsForType(Long.class, LConstantValidator.class, LExcludedOfValidator.class, LIncludedInValidator.class, LMinMaxValidator.class, LUniqueValidator.class);
		putValidatorsForType(float.class, DConstantValidator.class, DExcludedOfValidator.class, DIncludedInValidator.class, DMinMaxValidator.class, DUniqueValidator.class);
		putValidatorsForType(Float.class, DConstantValidator.class, DExcludedOfValidator.class, DIncludedInValidator.class, DMinMaxValidator.class, DUniqueValidator.class);
		putValidatorsForType(Double.class, DConstantValidator.class, DExcludedOfValidator.class, DIncludedInValidator.class, DMinMaxValidator.class, DUniqueValidator.class);
		putValidatorsForType(double.class, DConstantValidator.class, DExcludedOfValidator.class, DIncludedInValidator.class, DMinMaxValidator.class, DUniqueValidator.class);

		putValidatorsForType(Collection.class, CollectionNotEmptyValidator.class, CollectionSizeValidator.class);
		defaultEnumValidatorType = Arrays.asList(EnumConstantValidator.class, EnumExcludedOfValidator.class, EnumIncludedInValidator.class, EnumUniqueValidator.class).toArray(new Class[0]);
	
		
		putValidatorsForFunction(VALIDATION_CONSTANT_FUNCTION, BooleanConstantValidator.class, StringConstantValidator.class, DateConstantValidator.class, DateTimeConstantValidator.class, LConstantValidator.class, DConstantValidator.class, EnumConstantValidator.class);
		putValidatorsForFunction(VALIDATION_UNIQUE_FUNCTION, StringUniqueValidator.class, DateUniqueValidator.class, LUniqueValidator.class, DUniqueValidator.class, EnumUniqueValidator.class);
		putValidatorsForFunction(VALIDATION_LENGTH_FUNCTION, StringLengthValidator.class, CollectionSizeValidator.class);
		putValidatorsForFunction(VALIDATION_MATCHES_FUNCTION, StringMatchesValidator.class);
		putValidatorsForFunction(VALIDATION_CONTAINS_FUNCTION, StringContainsValidator.class);
		putValidatorsForFunction(VALIDATION_NOT_CONTAINS_FUNCTION, StringNotContainsValidator.class);
		putValidatorsForFunction(VALIDATION_ENDS_WITH_FUNCTION, StringEndsWithValidator.class);
		putValidatorsForFunction(VALIDATION_NOT_ENDS_WITH_FUNCTION, StringNotEndsWithValidator.class);
		putValidatorsForFunction(VALIDATION_STARTS_WITH_FUNCTION, StringStartsWithValidator.class);
		putValidatorsForFunction(VALIDATION_NOT_STARTS_WITH_FUNCTION, StringNotStartsWithValidator.class);
		putValidatorsForFunction(VALIDATION_INCLUDED_IN_FUNCTION, StringIncludedInValidator.class, DateIncludedInValidator.class, DateTimeIncludedInValidator.class, LIncludedInValidator.class, DIncludedInValidator.class, EnumIncludedInValidator.class);
		putValidatorsForFunction(VALIDATION_EXCLUDED_OF_FUNCTION, StringExcludedOfValidator.class, DateExcludedOfValidator.class, DateTimeExcludedOfValidator.class, LExcludedOfValidator.class, DExcludedOfValidator.class, EnumExcludedOfValidator.class);
		putValidatorsForFunction(VALIDATION_AFTER_FUNCTION, AfterDateValidator.class);
		putValidatorsForFunction(VALIDATION_BEFORE_FUNCTION, BeforeDateValidator.class);
		putValidatorsForFunction(VALIDATION_NOT_STARTS_WITH_FUNCTION, StringNotStartsWithValidator.class);
		putValidatorsForFunction(VALIDATION_MIN_MAX_FUNCTION, LMinMaxValidator.class, DMinMaxValidator.class);
		putValidatorsForFunction(VALIDATION_NOT_EMPTY_FUNCTION, CollectionNotEmptyValidator.class);
	}

	public static void putValidatorsForType(Class<?> clazz, Class<? extends Validator>... validators) {
		mapValidatorByType.put(clazz, Arrays.asList(validators));
	}

	public static void putValidatorsForFunction(String function, Class<? extends Validator>... validators) {
		mapValidatorByFunction.put(function, Arrays.asList(validators));
	}
	
	public static List<Class<? extends Validator>> getValidatorsClassForType(Class<?> clazz) {
		if(clazz.isEnum())
			return mapValidatorByType.get(Enum.class);
		return mapValidatorByType.get(clazz);
	}
	
	public static List<Validator> instantiateValidatorsForType(Class<?> clazz) {
		List<Class<? extends Validator>> validators = mapValidatorByType.get(clazz);
		if(validators == null) {
			if(clazz.isEnum())
				return instantiateAllValidators(defaultEnumValidatorType);
			else if(clazz.isArray())
				return instantiateValidatorsForType(Collection.class);
			else 
				return new ArrayList<Validator>();
		}
		return instantiateAllValidators((Class<? extends Validator>[])validators.toArray());
	}
	
	public static Validator instantiateNewValidator(Class<? extends Validator> clazz) {
		try {
			Validator instance = clazz.newInstance();
			return instance;
		} catch (InstantiationException e) {
			throw new IllegalStateException("The Validator "+clazz+" must have an empty accessible constructor");
		} catch (IllegalAccessException e) {
			throw new IllegalStateException("The Validator "+clazz+" must have an empty accessible constructor");
		}

	}

	/**
	 * create a validatorby by instantiating clazzname
	 * @param clazzname the class name of the validator instantiate
	 * @return a validator
	 */
	public static Validator instantiateNewValidator(String clazzname) {
		try {
			return (Validator)Class.forName(clazzname).newInstance();
		} catch (ClassCastException ex) {
			throw new CsvParserException("The class "+clazzname+" is not a Validator.", ex);
		} catch (InstantiationException ex) {
			throw new CsvParserException("The class "+clazzname+" can't be instanciated.", ex);
		} catch (IllegalAccessException ex) {
			throw new CsvParserException("The class "+clazzname+" is not accessible.", ex);
		} catch (ClassNotFoundException ex) {
			throw new CsvParserException("The class "+clazzname+" is not found.", ex);
		}
	}

	public static List<Validator> instantiateAllValidators(Class<? extends Validator> ... classes) {
		List<Validator> result = new ArrayList<Validator>();		
		for(Class<? extends Validator> validatorClass : classes) {
			result.add(instantiateNewValidator(validatorClass));
		}
		return result;
	}

	public static List<Validator> instantiateAllValidators(String ... classes) {
		List<Validator> result = new ArrayList<Validator>();		
		for(String validatorClass : classes) {
			result.add(instantiateNewValidator(validatorClass));
		}
		return result;
	}
	
	public static ChainValidator createNewChainValidator(Context context, Field field, Class<? extends Validator>[] customValidators, boolean required) {
		ChainValidator chainValidator = new ChainValidator(required);

		List<Validator> validators = instantiateValidatorsForType(field.getType());
		if(customValidators.length != 0) {
			validators.addAll(instantiateAllValidators(customValidators));
		}
		
		for (Validator validator : validators) {
			if(validator.init(context, field))
				chainValidator.push(validator);
		}
		return chainValidator;
	}
	
	public static ChainValidator createNewChainValidator(Context context,  Field field, ValidationBasicOptions validationBasicOptions) {
		boolean requiredByAnnotation = context.isMappedByAnnotation() && context.getMappingManagerByAnnotation().checkRequiredOnMapping(field);
		boolean required = validationBasicOptions.isRequired() || requiredByAnnotation || field.isAnnotationPresent(Required.class);
		ChainValidator chainValidator = new ChainValidator(required);

		List<Validator> validators = instantiateValidatorsForType(field.getType());
		if(validationBasicOptions.getCustomValidators().length != 0)
			validators.addAll(instantiateAllValidators(validationBasicOptions.getCustomValidators()));
		
		for (Validator validator : validators) {
			if(validator.init(context, field))
				chainValidator.push(validator);
		}
		return chainValidator;
	}

	public static ChainValidator createNewChainValidatorOnEachValue(Context context,  Field field, ValidationBasicOptions validationBasicOptions) {
		boolean requiredByAnnotation = context.isMappedByAnnotation() && context.getMappingManagerByAnnotation().checkRequiredOnMapping(field);
		boolean required = validationBasicOptions.isRequired() || requiredByAnnotation || field.isAnnotationPresent(Required.class);
		ChainValidator chainValidator = new ChainValidator(required);

		List<Validator> validators = instantiateValidatorsForType(ReflectionUtils.getParameterizedClass(field));
		if(validationBasicOptions.getCustomValidators().length != 0)
			validators.addAll(instantiateAllValidators(validationBasicOptions.getCustomValidators()));
		
		for (Validator validator : validators) {
			if(validator.init(context, field))
				chainValidator.push(validator);
		}
		return chainValidator;
	}

	public static ChainValidator createNewChainValidator(Context context, Field field, XmlFieldMapping mapping) {
		ChainValidator chainValidator = new ChainValidator(mapping.isRequired());
		List<? extends XmlValidator> validators = mapping.getValidators();
		for (XmlValidator xmlValidator : validators) {
			Validator validator;
			if(XmlCustomValidator.class.equals(xmlValidator.getClass())) 
				validator = instantiateNewValidator(((XmlCustomValidator)xmlValidator).with());
			else
				validator = getValidatorByFunction(field.getType(), xmlValidator.getFunctionValidation());
			if(validator.init(context, field, xmlValidator))
				chainValidator.push(validator);
		}
		return chainValidator;
	}
	
	// TODO : à voir pour refactorer en une seule méthode avec la précédente avec en param : context, field, list, booleen
	public static ChainValidator createNewChainValidatorOnEachValue(Context context, Field field, XmlFieldMapping mapping) {
		ChainValidator chainValidator = new ChainValidator(mapping.isEachValueRequired());

		List<? extends XmlValidator> validators = mapping.getValidatorsOnEachValue();
		for (XmlValidator xmlValidator : validators) {
			Validator validator;
			if(XmlCustomValidator.class.equals(xmlValidator.getClass())) 
				validator = instantiateNewValidator(((XmlCustomValidator)xmlValidator).with());
			else
				validator = getValidatorByFunction(field.getType(), xmlValidator.getFunctionValidation());
			if(validator.init(context, field, xmlValidator))
				chainValidator.push(validator);
		}
		return chainValidator;		
	}
	
//	public static ChainValidator createNewChainValidator(XmlContext context, Field field, Element element, boolean required) {
//		ChainValidator chainValidator = new ChainValidator(required);
//
//		List<Validator> validators = instantiateValidatorsForType(ReflectionUtils.getParameterizedClass(field));
//		String[] customValidators = ValidatorFactory.extractValidatorClassesFromElement(element);
//		validators.addAll(ValidatorFactory.instantiateAllValidators(customValidators));
//		
//		for (Validator validator : validators) {
//			if(validator.init(context, field, element))
//				chainValidator.push(validator);
//		}
//		return chainValidator;
//	}
//	
	public static String[] extractValidatorClassesFromElement(Element element) {
		List<String> validators = new ArrayList<String>();
		Match validatorElements = $(element).child(Xml.VALIDATORS_ELEMENT).children(Xml.VALIDATOR_ELEMENT);
		if(!validatorElements.isEmpty()) {
			for (Match validator : validatorElements.each()) {
				validators.add(validator.text());
			}
		}
		return validators.toArray(new String[0]);
	}
	
	public static Class<? extends Annotation> getValidationAnnotationByType(Class<?> type) {
		if(type.isEnum())
			return mapAnnotationByType.get(Enum.class);
		return mapAnnotationByType.get(type);
	}
	
	public static Validator getValidatorByFunction(Class<?> type, String function) {
		List<Class<? extends Validator>> validatorsByType = getValidatorsClassForType(type);
		List<Class<? extends Validator>> validatorsByFunction = mapValidatorByFunction.get(function);
		
		for (Class<? extends Validator> validatorByFunction : validatorsByFunction) {
			for (Class<? extends Validator> validatorByType : validatorsByType) {
				if(validatorByFunction.equals(validatorByType))
					return instantiateNewValidator(validatorByType);
			}
		}
		
		return null;
	}
}
